package roborally.model;

import java.io.*;
import java.util.ArrayList;

import roborally.model.executables.*;
import roborally.model.executables.commands.*;
import roborally.model.evaluables.*;
import roborally.model.evaluables.conditions.*;

public class Program {
	
	/**
	 * The robot on which this program is executed.
	 */
	private final Robot myRobot;
	public Robot getRobot() { return myRobot; }
	
	/**
	 * A string representing the program as read in the sourcefile (.prog)
	 */
	private final String progStr;
	public String getProgramString() { return progStr; }
	
	/**
	 * The main Executable of this program. This is the structure (while, if or seq) that contains every other executable in the program.
	 */
	private final Executable mainExecutable;
	public Executable getExecutable(){ return mainExecutable; }
	
	/**
	 * This variable holds the reference to the last executed RobotCommand of this program.
	 * This is needed if we want to resume execution from this command.
	 */
	private RobotCommand lastExecutedRobotCommand;
	public RobotCommand getLastExecutedRobotCommand() { return lastExecutedRobotCommand; }
	public void setLastExecutedRobotCommand(RobotCommand robotCommand) { lastExecutedRobotCommand = robotCommand; }
	
	/**
	 * Constructor of a program, initializing the program with an associated robot and a filePath to its sourcefile.
	 * @param robot
	 * 			The associated robot.
	 * @param filePath
	 * 			The path to its sourcefile.
	 */
	public Program(Robot robot, String filePath) throws IOException, IllegalArgumentException{
		this.myRobot = robot; //associate robot
		String progStr = stringFromFile(filePath); //read the program into a string
		this.progStr = progStr.replaceAll("(\\s+)",""); //remove whitespaces and assign to progStr
		this.mainExecutable = ReadExecutable(getProgramString()); //load the program
		setLastExecutedRobotCommand(null);
	}
	
	/**
	 * @param 	filePath
	 * 			The path of to the file.
	 * @return	A string representing the contents of the file.
	 * @throws IOException
	 * 			If the file could not be read for whatever reason, an IOException is thrown.
	 */
	private String stringFromFile(String filePath) throws IOException{
		BufferedReader in = new BufferedReader(new FileReader(filePath));
		StringBuffer sb = new StringBuffer();
		String str = in.readLine();
		while (str != null){	
			sb.append(str);
			str = in.readLine();
		}
		in.close();
		return sb.toString();
	}
	
	/**
	 * A method which saves the string of the program in the specified directory.
	 * @param 	fileDirectory
	 * 			The path to the directory where the file should be stored.
	 * @throws IOException
	 * 			If the file could not be written for whatever reason, an IOException is thrown.
	 */
	public void saveProgram(String fileDirectory) throws IOException{
		Writer output = null;
		File file = new File(fileDirectory);
		output = new BufferedWriter(new FileWriter(file));
		output.write(getProgramString());
		output.close();
	}
	
	/**
	 * Reads a string representing an executable and creates the associated object as specified by the string.
	 * @param evaStr
	 * 			The string to be interpretated as an executable.
	 * @return	The interpretated Executable object.
	 * @throws IllegalArgumentException
	 * 			If the string could not be interpretated, an IllegalArgumentException is thrown.
	 */
	private Executable ReadExecutable(String exeStr) throws IllegalArgumentException{
		if(exeStr == null || exeStr.length() == 0) throw new IllegalArgumentException("The string given as parameter is null or empty.");
		IllegalArgumentException illArgExc = new IllegalArgumentException("The string \""+exeStr+"\" representing an Executable could not be interpretated. Check the program for syntax errors!"); //to be thrown if the string cannot be interpretated.
		
		if( (exeStr.charAt(0) == '(') && (exeStr.charAt(exeStr.length()-1) == ')') ) 
			exeStr = exeStr.substring(1, exeStr.length() - 1); //if the first and last character of the string are respectively '(' and ')', remove them
		
		ArrayList<String> arguments = SplitByTopLevelParantheses(exeStr);
		if(arguments.isEmpty()){ //There are no arguments (this means it must be a RobotCommand)
			if( exeStr.equals("move") ) return new Move(getRobot(), this);
			else if( exeStr.equals("pickup-and-use") ) return new PickupAndUse(getRobot(), this);
			else if( exeStr.equals("shoot") ) return new Shoot(getRobot(), this);
			else if( exeStr.contains("turn") ){
				String arg = exeStr.substring(4);
				if(arg.equals("counterclockwise")) return new Turn(getRobot(), this, false);
				else return new Turn(getRobot(), this, true);
			}
			else if( exeStr.contains("seq") ) return new Sequence(new ArrayList<Executable>()); //empty sequence
			else throw illArgExc;
		}
		else{ //There are arguments after the Executable's name, it is either a loop, selection or sequence Executable.
			String exeName = exeStr.substring(0, exeStr.indexOf('(')); //everything before the first '(' is the name of the Executable	
			if(exeName.equals("while") && arguments.size() == 2) 
				return new Loop(ReadEvaluable(arguments.get(0)), ReadExecutable(arguments.get(1)));
			else if(exeName.equals("if") && arguments.size() == 3)
				return new Selection(ReadEvaluable(arguments.get(0)), ReadExecutable(arguments.get(1)), ReadExecutable(arguments.get(2)));
			else if(exeName.equals("seq")){
				ArrayList<Executable> executables = new ArrayList<Executable>();
				for(String str : arguments)
					executables.add(ReadExecutable(str));
				return new Sequence(executables);
			}
			else throw illArgExc;
		}
	}
	
	/**
	 * Reads a string representing an evaluable and creates the associated object as specified by the string.
	 * @param evaStr
	 * 			The string to be interpretated as an evaluable.
	 * @return	The interpretated Evaluable object.
	 * @throws IllegalArgumentException
	 * 			If the string could not be interpretated, an IllegalArgumentException is thrown.
	 */
	private Evaluable ReadEvaluable(String evaStr) throws IllegalArgumentException{
		if(evaStr == null || evaStr.length() == 0) throw new IllegalArgumentException("The string given as parameter is null or empty.");
		IllegalArgumentException illArgExc = new IllegalArgumentException("The string \""+evaStr+"\" representing an Evaluable could not be interpretated."); //to be thrown if the string cannot be interpretated.
		
		if( (evaStr.charAt(0) == '(') && (evaStr.charAt(evaStr.length()-1) == ')') ) 
			evaStr = evaStr.substring(1, evaStr.length() - 1); //if the first and last character of the string are respectively '(' and ')', remove them
		
		ArrayList<String> arguments = SplitByTopLevelParantheses(evaStr);
		if(arguments.isEmpty()){ //There are no arguments (this means it must be a RobotCondition or True)
			if( evaStr.equals("true") ) return new True();
			else if( evaStr.equals("at-item") ) return new AtItem(getRobot());
			else if( evaStr.equals("can-hit-robot") ) return new CanHitRobot(getRobot());
			else if( evaStr.equals("wall") ) return new WallOnRight(getRobot());
			else if( evaStr.contains("energy-at-least") ){
				double arg = Double.parseDouble(evaStr.substring(15));
				if(arg >= 0)return new EnergyAtLeast(getRobot(), new EnergyAmount(arg)); //check preconditions of constructor of EnergyAmount!
				else return new EnergyAtLeast(getRobot(), new EnergyAmount(0)); //if a negative argument is given for energy-at-least, we interpretate it as 0
			}
			else throw illArgExc;
		}
		else{ //There are arguments after the Evaluable's name, it is either a conjunction, disjunction or negation Evaluable.
			String evaName = evaStr.substring(0, evaStr.indexOf('(')); //everything before the first '(' is the name of the Executable	
			if(evaName.equals("and")){
				ArrayList<Evaluable> evaluables = new ArrayList<Evaluable>();
				for(String arg : arguments)
					evaluables.add(ReadEvaluable(arg));
				return new Conjunction(evaluables);
			}
			else if(evaName.equals("or")) {
				ArrayList<Evaluable> evaluables = new ArrayList<Evaluable>();
				for(String arg : arguments)
					evaluables.add(ReadEvaluable(arg));
				return new Disjunction(evaluables);
			}
			else if(evaName.equals("not") && arguments.size() == 1) 
				return new Negation( ReadEvaluable(arguments.get(0)) );
			else throw illArgExc;
		}
	}
	
	/**
	 * Splits a string into the components surrounded by the top-level parantheses of the string.
	 * @param 	str 
	 * 			The string which needs to be split into the components between the top-level parantheses.
	 * @return	An array of strings, holding the split components.
	 */
	private static ArrayList<String> SplitByTopLevelParantheses(String str){
		ArrayList<String> strList = new ArrayList<String>();
		char[] charArr = str.toCharArray();
		int opened = 0;
		int indexOpen = str.indexOf('(');//beginIndex holds the index of the first next '('
		for(int i = indexOpen; i < charArr.length && indexOpen >= 0; ++i) { //as long as i is a valid index for 'str' and there is at least one opening paranthese left (indexOpen >= 0), execute the body of the for:
			if(charArr[i] == '(') ++opened; //if the current char is a '(', increment the amount of opened parantheses
			else if(charArr[i] == ')') --opened; //if the current char is a ')' decrement the amount of opened parantheses
			if(opened == 0 && indexOpen+1 <= i){ //if no parantheses are opened and if the index of the first char after the opening paranthese (indexOpen + 1) is smaller than the index of the closing paranthese
				strList.add(str.substring(indexOpen+1,i)); //add the substring between the parantheses to the list
				indexOpen = str.substring(i).indexOf('(');//we set the new indexOpen to the first appereance of '(' in the substring that is left.
				if(indexOpen >= 0) indexOpen += i; //unless there was no more opening paranthese found, we still need to add the current index to where the next paranthese was found in the substring. 
			}
		}
		return strList;
	}
	
	/**
	 * By invoking this method, the next RobotCommand in this program will be executed. (At least if there is a next RobotCommand to be executed)
	 * @effect |getExecutable().executeNextRobotCommand(getLastExecutedRobotCommand())
	 */
	public void step(){
		boolean result = getExecutable().executeNextRobotCommand(getLastExecutedRobotCommand());
		if(result) System.out.print("The next RobotCommand["+ getLastExecutedRobotCommand().toString() +"] was executed.\n");
		else System.out.print("No RobotCommand was executed.)\n"); //Either the program has come to a final state, or no RobotCommand was executed due to conditions depending on the state of the game
	}
	/**
	 * Executes step() n times.
	 */
	public void stepn(int n){
		for(int i = 0; i < n; ++i){
			System.out.print(i+1+". ");
			step();
		}
	}
	
//	//for debugging purposes
//	public static void main(String[] args) throws IOException
//	{
//		//Example SplitByTopLevelParantheses(string)
//		String s = "(seq (move) (if (can-hit-robot) (shoot) (turn clockwise)) (while (energy-at-least 5000) (seq (turn clockwise) (move) (move))) (move) (move))";
//		String s_2 = "seq (move) (if (can-hit-robot) (shoot) (turn clockwise)) (while (energy-at-least 5000) (seq (turn clockwise) (move) (move))) (move) (move)";
//		for( String str : SplitByTopLevelParantheses(s))
//			System.out.println(str);
//		for( String str : SplitByTopLevelParantheses(s_2))
//			System.out.println(str);
//		String noArg = "pickup-and-use";
//		System.out.println(SplitByTopLevelParantheses(noArg).length);
//	}
//	public static void main(String[] args) throws IOException
//	{
//		Robot rob = new Robot();
//		Board board = new Board(20,20);
//		board.putElement(new Position(0,10),rob);
//		board.putElement(new Position(3,11), new Wall());
//		board.putElement(new Position(3,8), new Battery());
//		board.putElement(new Position(4,6), new Wall());
//		board.putElement(new Position(0,6), new Robot());
//		rob.loadProgram(rob.getClass().getClassLoader().getResource("roborally/programs/jonas1.prog").getPath());
//		rob.getProgram().stepn(20); //YESH, it's alive!
//	}

}
